﻿using System;
using System.Collections.Generic;
using System.Linq;
using PgBirdie.Core.Config;
using PgBirdie.Core.Extensions;
using PgBirdie.Core.Model;
using PgBirdie.Core.Model.Database.Data;
using PgBirdie.Core.Model.Database.Structure;
using PgBirdie.Core.Model.Database.Structure.Constraint;
using PgBirdie.Core.Model.Database.Structure.Index;
using PgBirdie.Core.Service.DbObject;

namespace PgBirdie.Core.Service.Compare
{
    public class CompareExtractService : ICompareExtractService
    {
        private readonly ICompareService _compare;
        private readonly IDbObjectFromDbService _dbObjectFromDb;

        public CompareExtractService(ICompareService compare, IDbObjectFromDbService dbObjectFromDb)
        {
            _compare = compare;
            _dbObjectFromDb = dbObjectFromDb;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgSchema>> GetSchemas(PgDatabase source, PgDatabase target)
        {
            var keep = new Dictionary<string, PgSchema>();
            var add = new Dictionary<string, PgSchema>();
            var delete = new Dictionary<string, PgSchema>();

            foreach (var schema in source.Schemas)
                if (target.Schemas.ContainsKey(schema.Key))
                    keep.Add(schema.Key, schema.Value);
                else
                    add.Add(schema.Key, schema.Value);

            foreach (var schema in target.Schemas)
                if (!keep.ContainsKey(schema.Key) && !add.ContainsKey(schema.Key))
                    delete.Add(schema.Key, schema.Value);

            var items = new Dictionary<PersistenceType, IDictionary<string, PgSchema>>();
            items.Add(PersistenceType.Keep, keep);
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgSequence>> GetSequences(PgDatabase source, PgDatabase target)
        {
            var keep = new Dictionary<string, PgSequence>();
            var add = new Dictionary<string, PgSequence>();
            var delete = new Dictionary<string, PgSequence>();

            var sourceSequences = _dbObjectFromDb.GetAllSequences(source);
            var targetSequences = _dbObjectFromDb.GetAllSequences(target);

            foreach (var sequence in sourceSequences)
                if (targetSequences.ContainsKey(sequence.Key))
                    keep.Add(sequence.Key, sequence.Value);
                else
                    add.Add(sequence.Key, sequence.Value);

            foreach (var sequence in targetSequences)
                if (!keep.ContainsKey(sequence.Key) && !add.ContainsKey(sequence.Key))
                    delete.Add(sequence.Key, sequence.Value);

            var items = new Dictionary<PersistenceType, IDictionary<string, PgSequence>>();
            items.Add(PersistenceType.Keep, keep);
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgTable>> GetTables(PgDatabase source, PgDatabase target)
        {
            var keep = new Dictionary<string, PgTable>();
            var add = new Dictionary<string, PgTable>();
            var delete = new Dictionary<string, PgTable>();

            var sourceTables = _dbObjectFromDb.GetAllTables(source);
            var targetTables = _dbObjectFromDb.GetAllTables(target);

            foreach (var table in sourceTables)
                if (targetTables.ContainsKey(table.Key))
                    keep.Add(table.Key, table.Value);
                else
                    add.Add(table.Key, table.Value);

            foreach (var table in targetTables)
                if (!keep.ContainsKey(table.Key) && !add.ContainsKey(table.Key))
                    delete.Add(table.Key, table.Value);

            var items = new Dictionary<PersistenceType, IDictionary<string, PgTable>>();
            items.Add(PersistenceType.Keep, keep);
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgColumn>> GetColumns(PgDatabase source, PgDatabase target)
        {
            var nochange = new Dictionary<string, PgColumn>();
            var change = new Dictionary<string, PgColumn>();
            var add = new Dictionary<string, PgColumn>();
            var delete = new Dictionary<string, PgColumn>();

            var sourceColumns = _dbObjectFromDb.GetAllColumns(source);
            var targetColumns = _dbObjectFromDb.GetAllColumns(target);

            foreach (var column in sourceColumns.Values)
                if (targetColumns.ContainsKey(column.Name))
                {
                    var targetColumn = targetColumns[column.Name];
                    
                    var columnPropertiesMatch = _compare.ColumnPropertiesMatch(column, targetColumn);
                    var columnTypePropertiesMatch = _compare.ColumnTypePropertiesMatch(column, targetColumn);
                    var columnIsNullableMatch = !_compare.ColumnIsNullableMatch(column, targetColumn);

                    if (!columnPropertiesMatch || !columnTypePropertiesMatch || !columnIsNullableMatch)
                        change.Add(column.Name, column);
                    else
                        nochange.Add(column.Name, column);
                }
                else
                    add.Add(column.Name, column);

            foreach (var column in targetColumns)
                if (!nochange.ContainsKey(column.Key) && !change.ContainsKey(column.Key) && !add.ContainsKey(column.Key))
                    delete.Add(column.Key, column.Value);

            var items = new Dictionary<PersistenceType, IDictionary<string, PgColumn>>();
            //items.Add(PersistenceType.Keep, keep);
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Change, change);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgIndex>> GetIndexes(PgDatabase source, PgDatabase target)
        {
            var keep = new Dictionary<string, PgIndex>();
            var add = new Dictionary<string, PgIndex>();
            var delete = new Dictionary<string, PgIndex>();

            var sourceIndexes = _dbObjectFromDb.GetAllIndexes(source);
            var targetIndexes = _dbObjectFromDb.GetAllIndexes(target);

            foreach (var index in sourceIndexes)
                if (targetIndexes.ContainsKey(index.Key))
                    keep.Add(index.Key, index.Value);
                else
                    add.Add(index.Key, index.Value);

            foreach (var index in targetIndexes)
                if (!keep.ContainsKey(index.Key) && !add.ContainsKey(index.Key))
                    delete.Add(index.Key, index.Value);

            var items = new Dictionary<PersistenceType, IDictionary<string, PgIndex>>();
            items.Add(PersistenceType.Keep, keep);
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgConstraintBase>> GetConstraints(PgDatabase source, PgDatabase target)
        {
            var add = new Dictionary<string, PgConstraintBase>();
            var delete = new Dictionary<string, PgConstraintBase>();

            var sourceConstraints = _dbObjectFromDb.GetAllConstraints(source);
            var targetConstraints = _dbObjectFromDb.GetAllConstraints(target);

            foreach (var constraint in sourceConstraints.Values)
            {
                if (!targetConstraints.ContainsKey(constraint.Uid))
                {
                    add.Add(constraint.Uid, constraint);
                    continue;
                }

                var targetConstraint = targetConstraints[constraint.Uid];

                if (constraint.ConstraintType == ConstraintType.PrimaryKey)
                {
                    var key = (PgPrimaryKey)constraint;
                    var targetKey = (PgPrimaryKey)constraint;

                    if (key.Uid != targetKey.Uid ||
                        key.ColumnNames.Count != targetKey.ColumnNames.Count ||
                        targetKey.ColumnNames.Except(key.ColumnNames).Any())
                    {
                        add.Add(key.Uid, key);
                        delete.Add(targetKey.Uid, targetKey);
                        continue;
                    }
                }

                if (constraint.ConstraintType == ConstraintType.UniqueKey)
                {
                    var key = (PgUniqueKey)constraint;
                    var targetKey = (PgUniqueKey)constraint;

                    if (key.Uid != targetKey.Uid ||
                        key.ColumnNames.Count != targetKey.ColumnNames.Count ||
                        targetKey.ColumnNames.Except(key.ColumnNames).Any())
                    {
                        add.Add(key.Uid, key);
                        delete.Add(targetKey.Uid, targetKey);
                        continue;
                    }
                }

                if (constraint.ConstraintType == ConstraintType.ForeignKey)
                {
                    var key = (PgForeignKey)constraint;
                    var targetKey = (PgForeignKey)targetConstraint;

                    if (key.Uid != targetKey.Uid ||
                        key.SourceColumnName != targetKey.SourceColumnName ||
                        key.ReferenceTableName != targetKey.ReferenceTableName ||
                        key.ReferenceColumnName != targetKey.ReferenceColumnName)
                    {
                        add.Add(key.Uid, key);
                        delete.Add(targetKey.Uid, targetKey);
                        continue;
                    }
                }
            }

            foreach (var constraint in targetConstraints.Values)
            {
                if (!sourceConstraints.ContainsKey(constraint.Uid))
                    delete.Add(constraint.Uid, constraint);
            }            

            var items = new Dictionary<PersistenceType, IDictionary<string, PgConstraintBase>>();
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgDataRow>> GetDataRows(PgDatabase source, PgDatabase target)
        {
            var nochange = new Dictionary<string, PgDataRow>();

            var add = new Dictionary<string, PgDataRow>();
            var change = new Dictionary<string, PgDataRow>();
            var delete = new Dictionary<string, PgDataRow>();

            foreach (var schema in source.Schemas.Values)
            {
                if (!target.Schemas.ContainsKey(schema.Name))
                    continue;

                foreach (var table in schema.Tables.Values)
                {
                    if (!target.Schemas[schema.Name].Tables.ContainsKey(table.Name))
                    {
                        if (Settings.Source.Schemas[schema.Name].Tables[table.Name].WithData)
                            foreach (var dataRow in table.DataRows)
                                add.Add(dataRow.Key, dataRow.Value);
                        continue;
                    }

                    var targetTable = target.Schemas[schema.Name].Tables[table.Name];

                    foreach (var dataRow in table.DataRows)
                        if (targetTable.DataRows.ContainsKey(dataRow.Key))
                        {
                            var hasChanged = false;

                            var targetDataRow = targetTable.DataRows[dataRow.Key];

                            foreach (var dataColumn in dataRow.Value.DataColumns)
                            {
                                if (!targetDataRow.DataColumns.ContainsKey(dataColumn.Key))
                                    continue;

                                if (ExcludeColumn(dataColumn.Value))
                                    continue;

                                var targetDataColumn = targetDataRow.DataColumns[dataColumn.Key];

                                if (dataColumn.Value.Value.Equals(targetDataColumn.Value, StringComparison.Ordinal))
                                    continue;

                                hasChanged = true;
                                break;
                            }

                            if (hasChanged)
                                change.Add(dataRow.Key, dataRow.Value);
                            else
                                nochange.Add(dataRow.Key, dataRow.Value);
                        }
                        else
                            add.Add(dataRow.Key, dataRow.Value);
                }
            }

            foreach (var schema in target.Schemas)
                foreach (var table in schema.Value.Tables)
                    foreach (var dataRow in table.Value.DataRows)
                        if (!nochange.ContainsKey(dataRow.Key) && !change.ContainsKey(dataRow.Key) && !add.ContainsKey(dataRow.Key))
                            delete.Add(dataRow.Key, dataRow.Value);

            var items = new Dictionary<PersistenceType, IDictionary<string, PgDataRow>>();
            //items.Add(PersistenceType.Keep, keep);
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Change, change);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        public IDictionary<PersistenceType, IDictionary<string, PgColumn>> GetColumnsFromTable(PgTable source, PgTable target)
        {
            var keep = new Dictionary<string, PgColumn>();
            var add = new Dictionary<string, PgColumn>();
            var delete = new Dictionary<string, PgColumn>();

            foreach (var column in source.Columns)
                if (target.Columns.ContainsKey(column.Key))
                    keep.Add(column.Key, column.Value);
                else
                    add.Add(column.Key, column.Value);

            foreach (var column in target.Columns)
                if (!keep.ContainsKey(column.Key) && !add.ContainsKey(column.Key))
                    delete.Add(column.Key, column.Value);

            var items = new Dictionary<PersistenceType, IDictionary<string, PgColumn>>();
            items.Add(PersistenceType.Keep, keep);
            items.Add(PersistenceType.Add, add);
            items.Add(PersistenceType.Delete, delete);

            return items;
        }

        private bool ExcludeColumn(PgDataColumn dataColumn)
        {
            var column = dataColumn;
            var names = Settings.Source.Schemas[column.Table.Schema.Name].Tables[column.Table.Name].ExcludeStaticDataColumnNames;
            if (names.IsNullOrEmpty())
                return false;

            if (names.Contains(column.Name))
                return true;

            return false;
        }
    }
}